1 /** 2 Property-based testing. 3 */ 4 module unit_threaded.property; 5 6 template from(string moduleName) { 7 mixin("import from = " ~ moduleName ~ ";"); 8 } 9 10 /// 11 class PropertyException : Exception { 12 this(in string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow { 13 super(msg, file, line, next); 14 } 15 } 16 17 /** 18 Check that bool-returning F is true with randomly generated values. 19 */ 20 void check(alias F, int numFuncCalls = 100)(in uint seed = from!"std.random".unpredictableSeed, 21 in string file = __FILE__, in size_t line = __LINE__) @trusted { 22 23 import unit_threaded.randomized.random : RndValueGen; 24 import unit_threaded.should : UnitTestException; 25 import std.conv : text; 26 import std.traits : ReturnType, Parameters, isSomeString; 27 import std.array : join; 28 import std.typecons : Flag, Yes, No; 29 import std.random : Random; 30 31 static assert(is(ReturnType!F == bool), 32 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 33 34 auto random = Random(seed); 35 auto gen = RndValueGen!(Parameters!F)(&random); 36 37 auto input(Flag!"shrink" shrink = Yes.shrink) { 38 string[] ret; 39 static if (Parameters!F.length == 1 && canShrink!(Parameters!F[0])) { 40 auto val = gen.values[0].value; 41 auto shrunk = shrink ? val.shrink!F : val; 42 ret ~= shrunk.text; 43 static if (isSomeString!(Parameters!F[0])) 44 ret[$ - 1] = `"` ~ ret[$ - 1] ~ `"`; 45 } else 46 foreach (ref valueGen; gen.values) { 47 ret ~= valueGen.text; 48 } 49 return ret.join(", "); 50 } 51 52 foreach (i; 0 .. numFuncCalls) { 53 bool pass; 54 55 try { 56 gen.genValues; 57 } catch (Throwable t) { 58 throw new PropertyException("Error generating values\n" ~ t.toString, file, line, t); 59 } 60 61 try { 62 pass = F(gen.values); 63 } catch (Throwable t) { 64 // trying to shrink when an exeption is thrown is too much of a bother code-wise 65 throw new UnitTestException(text("Property threw. Seed: ", seed, 66 ". Input: ", input(No.shrink), ". Message: ", t.msg), file, line, t,); 67 } 68 69 if (!pass) { 70 throw new UnitTestException(text("Property failed. Seed: ", seed, 71 ". Input: ", input), file, line); 72 } 73 } 74 } 75 76 /** 77 For values that unit-threaded doesn't know how to generate, test that the Predicate 78 holds, using Generator to come up with new values. 79 */ 80 void checkCustom(alias Generator, alias Predicate)(int numFuncCalls = 100, 81 in string file = __FILE__, in size_t line = __LINE__) @trusted { 82 83 import unit_threaded.should : UnitTestException; 84 import std.conv : text; 85 import std.traits : ReturnType; 86 87 static assert(is(ReturnType!Predicate == bool), 88 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 89 90 alias Type = ReturnType!Generator; 91 92 foreach (i; 0 .. numFuncCalls) { 93 94 Type object; 95 96 try { 97 object = Generator(); 98 } catch (Throwable t) { 99 throw new PropertyException("Error generating value\n" ~ t.toString, file, line, t); 100 } 101 102 bool pass; 103 104 try { 105 pass = Predicate(object); 106 } catch (Throwable t) { 107 throw new UnitTestException(text("Property threw. Input: ", object, 108 ". Message: ", t.msg), file, line, t,); 109 } 110 111 if (!pass) { 112 throw new UnitTestException("Property failed with input:" ~ object.text, file, line); 113 } 114 } 115 } 116 117 private auto shrinkOne(alias F, int index, T)(T values) { 118 import std.stdio; 119 import std.traits; 120 121 auto orig = values[index]; 122 return shrink!((a) { values[index] = a; return F(values.expand); })(orig); 123 124 } 125 126 /// 127 @("Verify identity property for int[] succeeds") 128 @safe unittest { 129 130 int numCalls; 131 bool identity(int[] a) pure { 132 ++numCalls; 133 return a == a; 134 } 135 136 check!identity; 137 assert(numCalls == 100); 138 139 numCalls = 0; 140 check!(identity, 10); 141 assert(numCalls == 10); 142 } 143 144 /// 145 @("Explicit Gen") 146 @safe unittest { 147 import unit_threaded.randomized.gen; 148 import unit_threaded.should : UnitTestException; 149 import std.exception : assertThrown; 150 151 check!((Gen!(int, 1, 1) a) => a == 1); 152 assertThrown!UnitTestException(check!((Gen!(int, 1, 1) a) => a == 2)); 153 } 154 155 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init)); 156 157 T shrink(alias F, T)(T value) { 158 import std.conv : text; 159 160 assert(!F(value), text("Property did not fail for value ", value)); 161 162 T[][] oldParams; 163 return shrinkImpl!F(value, [value], oldParams); 164 } 165 166 private T shrinkImpl(alias F, T)(in T value, T[] candidates, T[][] oldParams = []) 167 if (from!"std.traits".isIntegral!T) { 168 import std.algorithm : canFind, minPos; 169 import std.traits : isSigned; 170 171 auto params = value ~ candidates; 172 if (oldParams.canFind(params)) 173 return value; 174 oldParams ~= params; 175 176 // if it suddenly starts passing we've found our boundary value 177 if (value < T.max && F(value + 1)) 178 return value; 179 if (value > T.min && F(value - 1)) 180 return value; 181 182 bool stillFails(T attempt) { 183 if (!F(attempt) && !candidates.canFind(attempt)) { 184 candidates ~= attempt; 185 return true; 186 } 187 188 return false; 189 } 190 191 T[] attempts; 192 if (value != 0) { 193 static if (isSigned!T) 194 attempts ~= -value; 195 attempts ~= value / 2; 196 } 197 if (value < T.max / 2) 198 attempts ~= cast(T)(value * 2); 199 if (value < T.max) 200 attempts ~= cast(T)(value + 1); 201 if (value > T.min) 202 attempts ~= cast(T)(value - 1); 203 204 foreach (attempt; attempts) 205 if (stillFails(attempt)) 206 return shrinkImpl!F(attempt, candidates, oldParams); 207 208 const min = candidates.minPos[0]; 209 const max = candidates.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0 210 211 if (!F(min)) 212 return shrinkImpl!F(min, candidates, oldParams); 213 if (!F(max)) 214 return shrinkImpl!F(max, candidates, oldParams); 215 216 return candidates[0]; 217 } 218 219 static assert(canShrink!int); 220 221 private T shrinkImpl(alias F, T)(T value, T[] candidates, T[][] oldParams = []) 222 if (from!"std.traits".isArray!T) { 223 if (value == []) 224 return value; 225 226 if (value.length == 1) { 227 T empty; 228 return !F(empty) ? empty : value; 229 } 230 231 auto fst = value[0 .. $ / 2]; 232 auto snd = value[$ / 2 .. $]; 233 if (!F(fst)) 234 return shrinkImpl!F(fst, candidates); 235 if (!F(snd)) 236 return shrinkImpl!F(snd, candidates); 237 238 if (F(value[0 .. $ - 1])) 239 return value[0 .. $ - 1]; 240 if (F(value[1 .. $])) 241 return value[1 .. $]; 242 243 if (!F(value[0 .. $ - 1])) 244 return shrinkImpl!F(value[0 .. $ - 1], candidates); 245 if (!F(value[1 .. $])) 246 return shrinkImpl!F(value[1 .. $], candidates); 247 return candidates[0]; 248 }